package com.lt.flashcard.webservice.rest;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import javax.servlet.ServletContext;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.bson.types.ObjectId;
import org.zkoss.image.AImage;
import org.zkoss.zkplus.hibernate.HibernateUtil;

import com.lt.flashcard.TextUtil;
import com.lt.flashcard.Utility;
import com.lt.flashcard.model.entity.Flashcard;
import com.lt.flashcard.model.entity.FlashcardSet;
import com.lt.flashcard.model.entity.Productcategory;
import com.lt.flashcard.model.entity.Tag;
import com.lt.flashcard.model.entity.User;
import com.lt.flashcard.model.service.AAService;
import com.lt.flashcard.model.service.EmailService;
import com.lt.flashcard.model.service.FlashcardService;
import com.lt.flashcard.model.service.FlashcardSetService;
import com.lt.flashcard.model.service.Repository;
import com.lt.flashcard.model.service.UserService;
import com.lt.flashcard.webservice.utils.ObjectUtils;
import com.lt.flashcard.webservice.utils.RestfulConstants;
import com.sun.jersey.multipart.FormDataParam;

@SuppressWarnings("deprecation")
@Path("")
public class RestfulService {
    private static final Logger log = Logger.getLogger(RestfulService.class);

    @Context
    ServletContext context;

    @POST
    @Path("flashcardSet/save")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Object saveFlashcardSet(@FormDataParam("id") Long id, @FormDataParam("name") String name,
            @FormDataParam("description") String description, @FormDataParam("image") InputStream image,
            @FormDataParam("tagIds") String tagIdsStr, @FormDataParam("authorId") Long authorId) {
        try {
            // Find set by ID in case of editing, otherwise create empty set.
            FlashcardSet set;
            if (id == null) {
                set = new FlashcardSet(name, description, null, FlashcardSet.STATUS_VALID, new Date(), "en", FlashcardSet.PRIVACY_PUBLIC,
                        0, 0, 0);
            } else {
                set = Repository.getInstance().getFlashcardSetService().findByID(id);
                if (set == null) {
                    return "Error: The flashcard set with id = " + id + " does not exist.";
                }
                set.setDescription(description);
            }

            // If image file has been sent, store it then retrieve the return name to set to card.
            if (image != null) {
                String imageName = saveImage(image);
                set.setImage(imageName);
            } else {
                set.setImage("noImage");
            }

            // Update Author
            User author = Repository.getInstance().getUserService().findByID(authorId);
            if (author == null) {
                return "Error: The user with id = " + authorId + " does not exist.";
            }
            set.setAuthor(author);

            // Attach tags to set
            List<Long> tagIds = getLongValuesFromParameter(tagIdsStr);
            List<Tag> tags = Repository.getInstance().getTagService().getByIds(tagIds);
            set.setTags(new HashSet<Tag>(tags));

            // Save set and get the result to build the return message
            boolean saveOK = Repository.getInstance().getFlashcardSetService().save(set);
            return saveOK ? set.getId() : RestfulConstants.MSG_UNABLE_SAVE_SET;
        } catch (Exception e) {
            log.error("Unable to save set: ", e);
            return RestfulConstants.MSG_UNABLE_SAVE_SET;
        }
    }

    @GET
    @Path("flashcardSet/list")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public Object listFlashcardSets(@QueryParam("authorId") Long authorId, @QueryParam("keyword") String keyword,
            @QueryParam("tagIds") String tagIdsStr, @DefaultValue("name") @QueryParam("sort") String sort,
            @DefaultValue("asc") @QueryParam("order") String order, @DefaultValue("25") @QueryParam("size") int size,
            @DefaultValue("0") @QueryParam("page") int page) {
        // Pre-populate parameters
        if ("topRate".equals(sort)) {
            sort = "voteResult";
        } else if ("topView".equals(sort)) {
            sort = "viewCount";
        } else if ("recent".equals(sort)) {
            sort = "created";
        }

        List<Long> tagIds = getLongValuesFromParameter(tagIdsStr);

        // Do searching
        List<FlashcardSet> sets = Repository.getInstance().getFlashcardSetService()
                .getAllFlashcardSets(authorId, size, page, sort, order, tagIds, keyword);

        // Transform returned data
        List<Map<String, Object>> data = ObjectUtils.extractPropertyValuesAsList(sets, RestfulConstants.FCS_LIST_PROPERTIES);
        return data;
    }

    @GET
    @Path("flashcardSet/detail/{id}")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public Object getFlashcardSetDetail(@PathParam("id") long id) {
        FlashcardSetService fcsService = Repository.getInstance().getFlashcardSetService();
        FlashcardSet set = fcsService.findByID(id);
        Map<String, Object> setData = new HashMap<String, Object>();
        if (set == null) {
            return "Error: The flashcard set with id = " + id + " does not exist.";
        }

        setData = ObjectUtils.extractPropertyValues(set, RestfulConstants.FCS_DETAIL_PROPERTIES);

        List<Flashcard> cards = set.getFlashcards();
        if (!cards.isEmpty()) {
            setData.put("cards", ObjectUtils.extractPropertyValuesAsList(cards, RestfulConstants.FC_LIST_PROPERTIES));
        }

        Set<Tag> tags = set.getTags();
        if (!tags.isEmpty()) {
            setData.put("tags", ObjectUtils.extractPropertyValuesAsList(new ArrayList<Tag>(tags), new String[] { "name" }));
        }

        User author = set.getAuthor();
        if (author != null) {
            setData.put("author", ObjectUtils.extractPropertyValues(author, new String[] { "id", "name" }));
        }

        String encodedName = TextUtil.removeAccent(set.getName()).toLowerCase();
        setData.put("linkToShare", Utility.getServerUrl().concat(String.format("/set/%d/%s", id, encodedName)));
        return setData;
    }

    @POST
    @Path("flashcardSet/{id}/clone")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public Object cloneFlashcardSet(@PathParam("id") long originalSetId, @FormParam("userId") long userId) {
        FlashcardSetService fcsService = Repository.getInstance().getFlashcardSetService();
        FlashcardSet originalSet = fcsService.findByID(originalSetId);
        if (originalSet == null) {
            return "Error: The flashcard set with id = " + originalSetId + " does not exist.";
        }

        if (originalSet.getAdmin() == FlashcardSet.ADMIN_YES || originalSet.getAdmin() == FlashcardSet.ADMIN_BUY) {
            return "Error: Cannot clone the system flashcard set (owned by administrator).";
        }

        User user = Repository.getInstance().getUserService().findByID(userId);
        if (user == null) {
            return "Error: The user with id = " + userId + " does not exist.";
        }

        FlashcardSet clonedSet = originalSet.clone();
        clonedSet.setAuthor(user);
        clonedSet.setAdmin(FlashcardSet.ADMIN_NO);
        boolean saveOK = fcsService.save(clonedSet);
        return saveOK ? clonedSet.getId() : "Error: Unable to clone set.";
    }

    @POST
    @Path("flashcard/save")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    public Object saveFlashcard(@FormDataParam("id") Long id, @FormDataParam("flashcardSetId") Long fcSetId,
            @FormDataParam("term") String term, @FormDataParam("definition") String definition, @FormDataParam("image") InputStream image) {
        try {
            boolean isCreated = id == null, saveOK = true;
            // Find card by ID in case of editing, otherwise create empty card.
            Flashcard card;
            if (isCreated) {
                card = new Flashcard(null, RestfulConstants.DEFAULT_NOTE, null, null, term, definition, 1, 0, new Date());
            } else {
                card = Repository.getInstance().getFlashcardService().findByID(id);
                if (card == null) {
                    return "Error: The flashcard with id = " + id + " does not exist.";
                }
                card.setDefinition(definition);
            }

            // If image file has been sent, store it then retrieve the return name to set to card.
            if (image != null) {
                String imageName = saveImage(image);
                card.setImage(imageName);
            } else {
                card.setImage("noImage");
            }

            // Save card and get the result to build the return message
            if (isCreated) {
                FlashcardSet fcSet = Repository.getInstance().getFlashcardSetService().findByID(fcSetId);
                if (fcSet == null) {
                    return "Error: The flashcard set with id = " + fcSetId + " does not exist.";
                }
                card.setFlashcardSet(fcSet);
                boolean saveCardOK = Repository.getInstance().getFlashcardService().save(card);

                if (!saveCardOK) {
                    return RestfulConstants.MSG_UNABLE_SAVE_CARD;
                } else {
                    fcSet.addFlashcard(card);
                    saveOK = Repository.getInstance().getFlashcardSetService().save(fcSet);
                }
            } else {
                saveOK = Repository.getInstance().getFlashcardService().save(card);
            }

            return saveOK ? card.getId() : RestfulConstants.MSG_UNABLE_SAVE_CARD;
        } catch (Exception e) {
            log.error("Unable to save card: ", e);
            return RestfulConstants.MSG_UNABLE_SAVE_CARD;
        }
    }

    @GET
    @Path("flashcard/list")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public Object listFlashcards(@QueryParam("setId") Long setId, @QueryParam("keyword") String keyword,
            @DefaultValue("term") @QueryParam("sort") String sort, @DefaultValue("asc") @QueryParam("order") String order,
            @DefaultValue("25") @QueryParam("size") int size, @QueryParam("page") int page) {
        if ("recent".equals(sort)) {
            sort = "created";
        }
        FlashcardService fcService = Repository.getInstance().getFlashcardService();
        List<Flashcard> cards = fcService.getFlashcardsFromFlashcardsetRestful(setId, keyword, sort, order, size, page);

        return ObjectUtils.extractPropertyValuesAsList(cards, RestfulConstants.FC_LIST_PROPERTIES);
    }

    @GET
    @Path("flashcard/detail/{id}")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public Object getFlashcardDetail(@PathParam("id") long id) {
        Flashcard card = Repository.getInstance().getFlashcardService().findByID(id);
        if (card == null) {
            return "Error: The flashcard with id = " + id + " does not exist.";
        } else {
            // TODO Should consider to use reflection like this: Flashcard.class.getDeclaredFields();
            return ObjectUtils.extractPropertyValues(card, RestfulConstants.FC_DETAIL_PROPERTIES);
        }
    }

    @POST
    @Produces(value = { MediaType.APPLICATION_JSON })
    @Path("flashcard/{id}/learnt")
    public Object learnFlashcard(@PathParam("id") long cardId) {
        try {
            Flashcard card = Repository.getInstance().getFlashcardService().findByID(cardId);
            if (card == null) {
                return "Error: The flashcard with id = " + cardId + " does not exist.";
            }
            Repository.getInstance().getLearningService().setFlashcardAsLearnt(card);
            return card.getLearnLevel();
        } catch (Exception e) {
            log.error("Error when updating learning status for card: ", e);
            return "Error: Unable to update learning status for card.";
        }
    }

    @POST
    @Produces(value = { MediaType.APPLICATION_JSON })
    @Path("flashcard/{id}/resetLearnStatus")
    public Object resetLearnStatus(@PathParam("id") long cardId) {
        try {
            Flashcard card = Repository.getInstance().getFlashcardService().findByID(cardId);
            if (card == null) {
                return "Error: The flashcard with id = " + cardId + " does not exist.";
            }
            Repository.getInstance().getLearningService().setFlashcardAsNotLearnt(card);
            return "notLearnt";
        } catch (Exception e) {
            log.error("Error when updating learning status for card: ", e);
            return "Error: Unable to update learning status for card.";
        }
    }

    @SuppressWarnings({ "unchecked" })
    @GET
    @Path("productCategory/list")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public List<Map<String, Object>> listProductCategories() {
        List<Productcategory> productCategories = HibernateUtil.getSessionFactory().getCurrentSession()
                .createCriteria(Productcategory.class).list();
        return ObjectUtils.extractPropertyValuesAsList(productCategories, new String[] { "id", "name" });
    }

    @GET
    @Path("tag/list")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public List<Map<String, Object>> listAdminTags() {
        List<Tag> adminTags = Repository.getInstance().getTagService().getAdminTagList();
        return ObjectUtils.extractPropertyValuesAsList(adminTags, new String[] { "id", "name" });
    }

    @GET
    @Path("dashboard/user/{id}")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public Object getDashboardInfo(@PathParam("id") Long id) {
        UserService userService = Repository.getInstance().getUserService();
        User user = userService.findByID(id);
        if (user == null) {
            return "Error: The user with id = " + id + " does not exist.";
        }
        Map<String, Object> dashboardData = new HashMap<String, Object>(6);
        dashboardData.put("blueCoin", user.getBlueCoin());
        dashboardData.put("rank", userService.getRank(user));
        int totalCards = userService.getTotalCardsOfUser(user);
        dashboardData.put("totalCards", totalCards);
        dashboardData.put("knownCards", user.getKnowCards());
        dashboardData.put("unknownCards", user.getUnknowCards());
        dashboardData.put("remainCards", totalCards - user.getKnowCards() - user.getUnknowCards());
        return dashboardData;
    }

    @POST
    @Path("login")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public Object login(@FormParam("username") String username, @FormParam("password") String password,
            @FormParam("remember") boolean remember) {
        boolean isAuthenticated = Repository.getInstance().getAAService().doLogin(username, password, remember, false);
        if (isAuthenticated) {
            User loggedInUser = Repository.getInstance().getUserService().findByUsername(username);
            return ObjectUtils.extractPropertyValues(loggedInUser, RestfulConstants.USER_DETAIL_PROPERTIES);
        } else {
            return "Error: Login failed.";
        }
    }

    @POST
    @Path("register")
    @Produces(value = { MediaType.APPLICATION_JSON })
    public Object
            register(@FormParam("username") String username, @FormParam("password") String password, @FormParam("email") String email) {
        UserService srv = Repository.getInstance().getUserService();
        try {
            if (srv.findByEmail(email) != null) {
                return "Error: This email already exists.";
            }
            if (srv.findByUsername(username) != null) {
                return "Error: This username already exists.";
            }

            User user = new User(username.toLowerCase(), password, email.toLowerCase());
            user.setBlock(0);
            user.setActivation("");
            user.setExpiredate(null);
            boolean saveOK = srv.save(user);

            if (!saveOK) {
                return "Error: Unable to register.";
            }

            EmailService esrv = Repository.getInstance().getEmailService();
            esrv.sendEmailWelcomeRestful(username.toLowerCase(), email.toLowerCase(), context.getRealPath("/"));

            AAService asrv = Repository.getInstance().getAAService();
            boolean isAuthenticated = asrv.doLogin(username.toLowerCase(), user.getPassword(), true, false);
            return isAuthenticated ? ObjectUtils.extractPropertyValues(user, RestfulConstants.USER_DETAIL_PROPERTIES)
                    : "Error: Unable to register.";
        } catch (Exception e) {
            log.error("Error when registering new user: ", e);
            return "Error: Unable to register.";
        }
    }

    @POST
    @Produces(value = { MediaType.APPLICATION_JSON })
    @Path("forgotPassword")
    public Object forgotPassword(@FormParam("email") String email) {
        try {
            UserService userService = Repository.getInstance().getUserService();
            User user = userService.findUserByEmail(email);
            if (user == null) {
                return "Error: The user with email = " + email + " does not exist.";
            }
            if (user.getBlock() == 1) {
                return "Error: Account is blocked.";
            }
            UUID idOne = UUID.randomUUID();
            user.setActivation(idOne.toString());
            Calendar cal = Calendar.getInstance();
            cal.setTime(new Date());
            cal.add(Calendar.DATE, 30);
            user.setExpiredate(cal.getTime());// expire date
            userService.save(user);

            // do send email
            EmailService emailService = Repository.getInstance().getEmailService();
            emailService.sendEmailResetPasswordRestful(user.getUsername(), user.getEmail(), user.getActivation(), context.getRealPath("/"));
            return ObjectUtils.extractPropertyValues(user, RestfulConstants.USER_DETAIL_PROPERTIES);
        } catch (Exception e) {
            log.error("Error when processing sending new password: ", e);
            return "Error: Unable to reset new password.";
        }
    }

    /**
     * Store image in server folder and return the auto-generated image name.
     * 
     * @param stream
     *            Binary stream of the image file
     * @return Generated image name
     */
    private String saveImage(InputStream stream) {
        String name = new ObjectId().toString();

        try {
            AImage a = new AImage("", stream);
            String fname = name + "." + a.getFormat();
            String fpath = Utility.getImageCache() + "/" + fname;
            String path = context.getRealPath("/") + fpath;
            File f = new File(path);

            if (!f.exists()) {
                FileOutputStream of = new FileOutputStream(f);

                InputStream s = a.getStreamData();

                byte[] buffer = new byte[1024];
                int len = s.read(buffer);
                while (len != -1) {
                    of.write(buffer, 0, len);
                    len = s.read(buffer);
                }
                of.close();
            }

            return fname;
        } catch (Exception e) {
            log.error("Can't save image to storage", e);
            return null;
        }
    }

    /**
     * Build list of long values from request parameter.
     * 
     * @param paramName
     *            Parameter name
     * @return Long values list
     */
    private List<Long> getLongValuesFromParameter(String paramName) {
        if (StringUtils.isBlank(paramName)) {
            return Collections.emptyList();
        }
        try {
            String[] arrTagIds = paramName.split(RestfulConstants.COMMA_JOIN);
            List<Long> tagIds = new ArrayList<Long>();
            for (String tagItem : arrTagIds) {
                if (StringUtils.isNotBlank(tagItem)) {
                    tagIds.add(Long.valueOf(tagItem));
                }
            }
            return tagIds;
        } catch (Exception e) {
            log.error("Error when parsing tag ids: ", e);
            return Collections.emptyList();
        }
    }
}